雑なAPIはtRPCで作り、ApiGateayでデプロイでいいかもしれない [デプロイ編]
はじめに
前回は実装まで、今回はApiGateayで動かしていきます。
Lambdaに上げるようにビルドする
今回はcdkを使いますが、Lambdaにあげるときに、node_moduleを含めたzipでアップロードします。
node_moduleはproductionだけ、開発環境はそのままにしときたいので、ビルド用のDockerImageを作っていきたいと思います。
また、tRPCをLambdaのHandlerで使うためのエンドポイントも作ります。
Lambdaのエンドポイントを作る
import { awsLambdaRequestHandler } from "@trpc/server/adapters/aws-lambda"; import { apiRouter } from "./trpc/router"; import { createApiContext } from "./trpc/context"; export const handler = awsLambdaRequestHandler({ router: apiRouter, createContext: createApiContext, });
tRPCがすでにAWS Lambda用のHandlerを用意してくれてるので、被せて終わりです。
Lambda用にアップロード用のzipを作る
FROM node:16 RUN apt-get update RUN apt-get install zip -y VOLUME /output WORKDIR /usr/src/app COPY . /usr/src/app RUN npm install RUN npm run build:src RUN npm install --omit=dev RUN cp -rf node_modules dist/node_modules RUN zip -9yr lambda.zip ./dist/ ENTRYPOINT ["cp", "lambda.zip", "/output/lambda.zip"]。
Lambda用のビルドコマンドも登録しておくと便利です
{ ... "scripts": { "build:lambda": "rm lambda.zip; docker build -t trpc-server .; docker run --rm --volume $PWD:/output trpc-server" }, ... }
ビルドしてみよう
npm run build:lambda
プロジェクトルートにlambda.zip
ができていれば成功です。
CDK構築
コンソールで構築しても良いんですが、雑APIはお片付けも簡単な方がいいです。作るのも消すのも簡単がベスト。
なのでCDKをつかって構築していきます。
CDKのプロジェクトを作成
プロジェクトルートにCDK用のプロジェクトを追加していきます。
$ mkdir formation $ cd formation $ cdk init --language typescript
API Gatewayを構築
今回はデフォルトであるFormationStackを書き換えていきます。API Gateway構築まで。
import { aws_iam as iam, aws_lambda as lambda, aws_apigateway as apigateway, Stack, StackProps, } from "aws-cdk-lib"; import { Construct } from "constructs"; import * as path from "path"; export class FormationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); const lambdaRole = new iam.Role(this, "TRpcSampleLambdaRole", { roleName: "TRpcSampleLambdaRole", assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName( "service-role/AWSLambdaBasicExecutionRole" ), ], }); const tRpcLambda = new lambda.Function(this, "TRpcLambda", { functionName: "trpc", runtime: lambda.Runtime.NODEJS_16_X, code: lambda.Code.fromAsset( path.join(__dirname, "../../server/lambda.zip") ), memorySize: 128, handler: "dist/index.handler", role: lambdaRole, environment: { NODE_ENV: "production", }, }); const api = new apigateway.RestApi(this, "TRpcApi", { restApiName: "TRpcApi", description: "TRpcApi", endpointTypes: [apigateway.EndpointType.EDGE], deployOptions: { stageName: "v1" }, }); const apiResource = api.root.addResource("api"); const anyResource = apiResource.addResource("{path+}"); anyResource.addMethod("any", new apigateway.LambdaIntegration(tRpcLambda)) } }
const apiResource = api.root.addResource("api"); const anyResource = apiResource.addResource("{path+}");`
Cloud Frontで /api
のときにApiGatewayを呼べるように設定するのapiのresourceを追加してます。
すべてのメソッドを受けるので {path+}
を追加します。
Cloud Frontを構築
import { aws_apigateway as apigateway, aws_cloudfront as cloudfront, aws_cloudfront_origins as origins, aws_iam as iam, aws_lambda as lambda, aws_s3 as s3, aws_s3_deployment as s3Deploy, Duration, RemovalPolicy, Stack, StackProps, } from "aws-cdk-lib"; import { Construct } from "constructs"; import * as path from "path"; export class FormationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); ... // 追記します。 const trpSampleBucket = new s3.Bucket(this, "TRpcSampleKamedonS3", { bucketName: "trpc-sample-kamedon", removalPolicy: RemovalPolicy.DESTROY, }); const originAccessIdentity = new cloudfront.OriginAccessIdentity( this, "OriginAccessIdentity", { comment: "website-distribution-originAccessIdentity", } ); const bucketPolicyStatement = new iam.PolicyStatement({ actions: ["s3:GetObject"], effect: iam.Effect.ALLOW, principals: [ new iam.CanonicalUserPrincipal( originAccessIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId ), ], resources: [trpSampleBucket.bucketArn + "/*"], }); trpSampleBucket.addToResourcePolicy(bucketPolicyStatement); const cachePolicy = new cloudfront.CachePolicy( this, "TRpcSampleCFCachePolicy", { queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(), } ); const distribution = new cloudfront.Distribution(this, "TRpcSampleCF", { defaultRootObject: "index.html", defaultBehavior: { allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD, cachePolicy: cachePolicy, viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, origin: new origins.S3Origin(trpSampleBucket, { originAccessIdentity: originAccessIdentity, }), }, additionalBehaviors: { "/api/*": { origin: new origins.RestApiOrigin(api), allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS, viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, cachePolicy: cachePolicy, }, }, priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL, minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021, }); new s3Deploy.BucketDeployment(this, "WebsiteDeploy", { sources: [ s3Deploy.Source.asset(path.join(__dirname, "../../client/dist")), ], destinationBucket: trpSampleBucket, distribution: distribution, distributionPaths: ["/*"], }); } }
CORS対応で /api
にApiGatewayのパスをあてます。全てのメソッド、全てのQueryをApiに渡せられるようにします。
ついでにクライアントもデプロイするようにしてます。
CDKでデプロイ
npx cdk deploy
Cloud FrontのURLにアクセスして動作確認します。
GETもPostも問題なく叩けています。
まとめ
ApiGatewayにデプロイしてみました。
Operating Lambda: イベント駆動型アーキテクチャにおけるアンチパターン ではあるので、プロダクションでつかうというより、雑API用途として使います。
tRPCはルーティングに特化してる感じがあり、今回のLambdaのhandlerのように、Next.jsで使う、Express、Fastifyなどで使うことができます。
雑に作って、そのまま他のフレームワークに移植することも簡単そうです。